﻿using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using System.Reflection;
using System.Text;
using Fintech.Utils;
using Fintech.Utils.Collections;
using Fintech.Utils.Diagnostics;
using ZOVNP.Model.Abstracts;

namespace ZOVNP.Engine
{
    /// <summary> Метаданные движка. </summary>
    internal class ZOVNPMetadata : IEngineMetadata
    {
        #region Поля

        /// <summary> Движок, метаданные которого представляет данный объект. </summary>
        private readonly ZOVNPEngine engine;
        /// <summary> Словарь связей внутренних классов сущностей БД с их внешними интерфейсами. </summary>
        private static BidirectionalDictionary<Type, Type> typeMapping;
        /// <summary> Словарь для всех свойств-таблиц сущностенй хранилища. </summary>
        private static Dictionary<Type, PropertyInfo> objectSets;
        /// <summary> Словарь, который хранит максимальные длины свойств сущностей. Создается один раз. </summary>
        private static Dictionary<Type, List<Tuple<string, int>>> typeMaxPropsLengths;
        private static Dictionary<Type, Tuple<string, string>> schemaTableMapping = new Dictionary<Type, Tuple<string, string>>();
        /// <summary> Объект блокировки для инициализации метаданых движка. </summary>
        private static object initializationLock = new object();
        /// <summary> Флаг завершения инициализации метаданных движка. </summary>
        private static bool initializationCompleted;
        private LazyLoader<string> realHostName;

        #endregion

        #region Конструкторы

        /// <summary> Создать экземпляр метаданных. </summary>
        /// <param name="Engine"> Используемый движок. </param>
        internal ZOVNPMetadata(ZOVNPEngine Engine)
        {
            Engine.CheckOnNull("Engine");
            engine = Engine;
            realHostName = new LazyLoader<string>(() => engine.ProviderAdapter.GetServerName());
            Initialize(engine);
        }

        #endregion

        #region Свойства

        /// <summary> Получить имя хранилища. </summary>
        public string Name { get { return engine.Name; } }

        /// <summary> Получить имя места размещения хранилища (это может быть псевдоним). </summary>
        public string HostName { get { return engine.HostName; } }

        /// <summary> Получить реальное имя места размещения хранилища. </summary>
        public string RealHostName { get { return realHostName.Value; } }

        /// <summary> Получить версию хоста. </summary>
        public string HostVersion { get { return engine.HostVersion; } }
        
        /// <summary> Словарь связей внутренних классов сущностей БД с их внешними интерфейсами. </summary>
        internal static BidirectionalDictionary<Type, Type> TypeMapping { get { return typeMapping; } }

        /// <summary> Словарь для всех свойств-таблиц сущностенй хранилища. </summary>
        internal static Dictionary<Type, PropertyInfo> ObjectSets { get { return objectSets; } }

        /// <summary> Словарь метаданных о максимальной длине (строковых) свойств доменных объектов. </summary>
        internal static Dictionary<Type, List<Tuple<string, int>>> MaxLengths { get { return typeMaxPropsLengths; } }

        #endregion

        #region Методы

        /// <summary> Инициализация метаданных движка. </summary>
        /// <param name="Engine"> Используемый движок. </param>
        private static void Initialize(ZOVNPEngine Engine)
        {
            if (initializationCompleted) return; // оптимизация для мгновенного возврата из метода без необходимости вызова Monitor.Enter()
            lock (initializationLock)
            {
                if (initializationCompleted) return; // защита от повторной инициализации
                typeMapping = new BidirectionalDictionary<Type, Type>();
                var types = typeof(ZOVNPContext).Assembly.GetExportedTypes().Where(it => it.IsSubType<DomainObject>()).ToArray();
                typeMaxPropsLengths = new Dictionary<Type, List<Tuple<string, int>>>();
                // в цикле происходит связывание сгенерированных классов сущностей БД с их внешними интерфейсами + кеширование метаданных
                foreach (var item in Engine.EntityConnection.GetMetadataWorkspace().GetItems(System.Data.Metadata.Edm.DataSpace.CSpace).OfType<System.Data.Metadata.Edm.EntityType>())
                {
                    string name = (string)item.MetadataProperties["Name"].Value;
                    var type = types.FirstOrDefault(it => it.Name == name);
                    if (type != null)
                    {
                        var attr = type.GetCustomAttributes(typeof(DomainObjectAttribute), false).Cast<DomainObjectAttribute>().FirstOrDefault();
                        if (attr != null && !typeMapping.ExistsForward(attr.DomainObjectInterfaceType))
                        {
                            typeMapping.AddAssociation(attr.DomainObjectInterfaceType, type);
                            var lengthProps = new List<Tuple<string, int>>();
                            foreach (var edmProp in item.Properties.OfType<System.Data.Metadata.Edm.EdmMember>())
                            {
                                var casted = edmProp.TypeUsage.EdmType as System.Data.Metadata.Edm.PrimitiveType;
                                if (casted != null && casted.ClrEquivalentType == typeof(string) || casted.ClrEquivalentType == typeof(byte[]))
                                {
                                    var facet = edmProp.TypeUsage.Facets["MaxLength"].Value;
                                    if (facet != null)
                                    {
                                        var facetType = facet.GetType();
                                        if (facetType == typeof(int))
                                            lengthProps.Add(new Tuple<string, int>(edmProp.Name, (int)facet));
                                        else if (string.Equals(facetType.Name, "Unbounded", StringComparison.InvariantCultureIgnoreCase))
                                            lengthProps.Add(new Tuple<string, int>(edmProp.Name, -1));
                                        else
                                            DebugTuner.Instance.Break(DebuggerLevel.Warning);
                                    }
                                }
                            }
                            if (lengthProps.Count > 0)
                                typeMaxPropsLengths.Add(attr.DomainObjectInterfaceType, lengthProps);
                        }
                    }
                }
                objectSets = new Dictionary<Type, PropertyInfo>();
                foreach (var property in typeof(ZOVNPContext).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(it => it.PropertyType.IsSubType<ObjectQuery>() && it.PropertyType.IsGenericType))
                    objectSets.Add(typeMapping.Backward(property.PropertyType.GetGenericArguments()[0]), property);
                initializationCompleted = true;
            }
        }

        /// <summary> Получение информации о максимальной длине строковых полей и полей-массивов доменных объектов. </summary>
        /// <typeparam name="T"> Тип доменного объекта. </typeparam>
        /// <param name="PropertyName"> Имя свойства, максимальную длину которого нужно получить. </param>
        /// <returns> Максимальная длина либо -1, если она не ограничена. </returns>
        public int MaxLength<T>(string PropertyName) { return MaxLength(typeof(T), PropertyName); }

        /// <summary> Получение информации о максимальной длине строковых полей и полей-массивов доменных объектов. </summary>
        /// <param name="EntityType"> Тип доменного объекта. </param>
        /// <param name="PropertyName"> Имя свойства, максимальную длину которого нужно получить. </param>
        /// <returns> Максимальная длина либо -1, если она не ограничена. </returns>
        public int MaxLength(Type EntityType, string PropertyName)
        {
            EntityType.CheckOnNull("EntityType");
            PropertyName.CheckOnNull("PropertyName");
            if (EntityType.IsClass && !TypeMapping.TryGetBackwardValue(EntityType, out EntityType)) return -1;
            System.Collections.Generic.List<Tuple<string, int>> properties;
            if (!MaxLengths.TryGetValue(EntityType, out properties)) return -1;
            var prop = properties.FirstOrDefault(it => it.Item1 == PropertyName);
            if (prop == null) return -1;
            return prop.Item2;
        }

        /// <summary> Получить название схемы базы данных и имени таблицы, где располагаются объекты заданного типа. </summary>
        /// <param name="ImplType"> Тип реализации (не интерфейса) доменного объекта. </param>
        /// <returns> Кортеж, состоящий из имени схемы и имени таблицы. </returns>
        internal Tuple<string, string> GetSchemaAndTable(Type ImplType)
        {
            engine.GuardDispose();
            ImplType.CheckOnNull("ImplType");

            while (ImplType.BaseType != typeof(DomainObject)) // Иначе в случае передачи дочернего класса в иерархии наследования будет сгенерировано исключение, так как метаданные описывают базовый класс.
                ImplType = ImplType.BaseType;

            string key = ImplType.Name;
            if (!typeMapping.ExistsBackward(ImplType)) throw new ArgumentException(string.Format(Properties.Resources.MSG_UnableToFindMetadataForType, key));
            Tuple<string, string> result = null;
            lock (schemaTableMapping)
            {
                if (!schemaTableMapping.TryGetValue(ImplType, out result))
                {
                    var container = engine.EntityConnection.GetMetadataWorkspace().GetEntityContainer("Skif_BPEntitiesStoreContainer", System.Data.Metadata.Edm.DataSpace.SSpace);
                    var targetSet = container.BaseEntitySets.FirstOrDefault(it =>
                    {
                        var casted = it as System.Data.Metadata.Edm.EntitySet;
                        if (casted == null) return false;
                        return string.CompareOrdinal(casted.ElementType.Name, key) == 0;
                    }) as System.Data.Metadata.Edm.EntitySet;
                    if (targetSet == null)
                        throw new System.ApplicationException(string.Format(Properties.Resources.MSG_UnableToFindMetadataForImplType, key));
                    string schema = (string)targetSet.MetadataProperties["Schema"].Value;
                    if (string.IsNullOrWhiteSpace(schema))
                        throw new System.InvalidOperationException(string.Format(Properties.Resources.MSG_SchemaAttributeNotSpecified, key));
                    string table = (string)targetSet.MetadataProperties["Table"].Value;
                    if (string.IsNullOrWhiteSpace(table)) table = targetSet.Name;
                    schemaTableMapping.Add(ImplType, result = new Tuple<string, string>(schema, table));
                }
            }
            return result;
        }

        #endregion
    }
}
